﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using CustomProvider;
using CustomProvider.Event;

namespace ClientSide.Communication
{
    /// <summary>
    /// The class for communication with the server
    /// </summary>
    public class AsynSocket
    {
        #region Fields

        /// <summary>
        /// Notifies main thread to proceed when connect is complete
        /// </summary>
        private AutoResetEvent _connectDone = new AutoResetEvent(false);

        /// <summary>
        /// Notifies main thread to proceed when send data is complete
        /// </summary>
        private AutoResetEvent _sendDone = new AutoResetEvent(false);

        /// <summary>
        /// Notifies main thread to proceed when receive data is complete
        /// </summary>
        private AutoResetEvent _receiveDone = new AutoResetEvent(false);

        /// <summary>
        /// Socket to communication with the Server
        /// </summary>
        private Socket _socClient;

        /// <summary>
        /// Contains the constants of this class
        /// </summary>
        private static class Const
        {
            /// <summary>
            /// Size of receive buffer.
            /// </summary>
            public const int BufferSize = 1024;

            /// <summary>
            /// The maximum length of the pending connections queue.
            /// </summary>
            public const int BackLog = 100;

            /// <summary>
            /// Establishment timeout
            /// </summary>
            public const int TIMEOUT = 5000;

            /// <summary>
            /// The message ESTABLISH_COMMUNICATION which is sent to the server
            /// </summary>
            public const string ESTABLISH_COMMUNICATION_MSG = "ESTABLISH_COMMUNICATION";

            /// <summary>
            /// The message REJECTCONNECT which is sent to the server
            /// </summary>
            public const string REJECTCONNECT_MSG = "REJECTCONNECT";

            /// <summary>
            /// The message STARTGAME which is sent from the server
            /// </summary>
            public const string STARTGAME_MSG = "STARTGAME";

            /// <summary>
            /// The message GUESSRESULT which is sent to the client
            /// </summary>
            public const string GUESSRESULT_MSG = "GUESSRESULT";

            /// <summary>
            /// The message GAMEFINISHED which is sent to the client
            /// </summary>
            public const string GAMEFINISHED_MSG = "GAMEFINISHED";

            /// <summary>
            /// The message OPPONENTDISCONNECT which is sent to the client
            /// </summary>
            public const string OPPONENTDISCONNECT_MSG = "OPPONENTDISCONNECT";

            /// <summary>
            /// The message UPDATE_ALREADY_PLAYERLIST which is sent to the client
            /// </summary>
            public const string UPDATE_ALREADY_PLAYERLIST_MSG = "UPDATE_ALREADY_PLAYERLIST";

            /// <summary>
            /// The message INVITETOPLAY which is sent from the client
            /// </summary>
            public const string INVITETOPLAY_MSG = "INVITETOPLAY";

            /// <summary>
            /// The message REJECT_INVITETOPLAY which is sent from the client
            /// </summary>
            public const string REJECT_INVITETOPLAY_MSG = "REJECT_INVITETOPLAY";
        }
        
        #endregion

        #region Methods

        /// <summary>
        /// Start connecting to the server
        /// </summary>
        /// <param name="ServerAddress">The IP Address of the server</param>
        /// <param name="ServerPort">The port on server which is opened for clients connect to</param>
        /// <param name="PlayerName">The name of the Player</param>
        /// <param name="OwnerIP">The IP Address of the Player</param>
        public void StartConnecting(string ServerAddress, int ServerPort, string PlayerName, string OwnerIP)
        {
            try
            {
                // Establish the server endpoint address for the socket.
                IPEndPoint remoteEP = new IPEndPoint(IPAddress.Parse(ServerAddress), ServerPort);

                // Create a TCP/IP socket.
                this._socClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                // Start to connect to the remote endpoint.
                this._socClient.BeginConnect(remoteEP, new AsyncCallback(ConnectCallback), this._socClient);
                // Wait for confirmation that the Client connected to the Server to proceed
                this._connectDone.WaitOne();

                // Send message establishment communication to the server.
                var idPlayer = string.Format("{0}@{1}", PlayerName, OwnerIP);
                this.Send(this._socClient, string.Format("{0}:{1}", Const.ESTABLISH_COMMUNICATION_MSG, idPlayer));
                // Wait for confirmation that the data sent from the Client to the Server to proceed
                this._sendDone.WaitOne();

                // Receive the response from the remote device.
                this.ReceiveMsg(this._socClient);

                // Wait confirmation of establisht communication in TIMEOUT
                if (this._receiveDone.WaitOne(Const.TIMEOUT) == false)
                    this.NotifyTimeOut();
            }
            catch (Exception ex)
            {
                this.NotifyDisplayMessageOnGUI(string.Format("Error on Communication: {0}", ex.ToString()));
            }
        }

        /// <summary>
        /// The method is invoked when the operation connect to server is complete
        /// </summary>
        /// <param name="ar"></param>
        private void ConnectCallback(IAsyncResult ar)
        {
            try
            {
                // Retrieve the socket from the state object.
                Socket client = (Socket)ar.AsyncState;
                // Complete the connection.
                client.EndConnect(ar);

                this.NotifyConnectStatus(true, string.Format("Socket connected to {0}", client.RemoteEndPoint.ToString()));

                // Signal that the connection has been made.
                _connectDone.Set();
            }
            catch (Exception ex)
            {
                this.NotifyConnectStatus(false, string.Format("The connection that connect to the server has not been established: {0}", ex.ToString()));
            }
        }

        /// <summary>
        /// Send asynchronous message to server
        /// </summary>
        /// <param name="Data">The message will be transfered to the server</param>
        public void SendSync(string Data)
        {
            try
            {
                // Convert the string data to byte data using ASCII encoding.
                byte[] byteData = Encoding.ASCII.GetBytes(Data);
                // Begin sending the data to the remote device.
                this._socClient.Send(byteData);
            }
            catch (Exception ex)
            {
                this.NotifyConnectStatus(false, string.Format("Error on SendMessage: {0}", ex.ToString()));
            }
        }

        /// <summary>
        /// Send asynchronous message to server
        /// </summary>
        /// <param name="Client">The socket which Clients use to communicate with the server</param>
        /// <param name="Data">The message will be transfered to the server</param>
        private void Send(Socket Client, string Data)
        {
            // Convert the string data to byte data using ASCII encoding.
            byte[] byteData = Encoding.ASCII.GetBytes(Data);

            // The action is invoked when client received message from server
            Action<IAsyncResult> actionSendCallback = (ar) =>
            {
                try
                {
                    // Retrieve the socket from the state object.
                    Socket client = (Socket)ar.AsyncState;
                    // Complete sending the data to the remote device.
                    int bytesSent = client.EndSend(ar);

                    // Signal that all bytes have been sent.
                    this._sendDone.Set();
                }
                catch (Exception ex)
                {
                    this.NotifyDisplayMessageOnGUI(string.Format("Error on SendCallback: {0}", ex.ToString()));
                }
            };
            // Begin sending the data to the remote device.
            Client.BeginSend(byteData, 0, byteData.Length, 0, new AsyncCallback(actionSendCallback), Client);
        }

        /// <summary>
        /// Start listening message which will be transfered from the server
        /// </summary>
        /// <param name="client">The socket to transfer message between client and server</param>
        private void ReceiveMsg(Socket client)
        {
            // Create the state object.
            StateObject state = new StateObject();
            state.WorkSocket = client;

            // Begin receiving the data from the remote device.
            client.BeginReceive(state.Buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state);
        }

        /// <summary>
        /// The method is invoked when client received message from server
        /// </summary>
        /// <param name="ar"></param>
        private void ReceiveCallback(IAsyncResult ar)
        {
            try
            {
                string content = string.Empty;

                // Retrieve the state object and the client socket 
                // from the asynchronous state object.
                StateObject state = (StateObject)ar.AsyncState;
                Socket client = state.WorkSocket;

                // Read data from the remote device.
                int bytesRead = client.EndReceive(ar);

                if (bytesRead > 0)
                {
                    // There might be more data, so store the data received so far.
                    state.ReceivedMsg.Append(Encoding.ASCII.GetString(state.Buffer, 0, bytesRead));
                    content = state.ReceivedMsg.ToString();
                    this.NotifyDisplayMessageOnGUI(string.Format("Received message from the Server: {0}", content));           
                    // Clear buffer
                    state.ReceivedMsg.Clear();

                    // Handle received message from the Client
                    this.MessageFromServerHandler(client, content);
                }

                // Ready to receive the next data.
                client.BeginReceive(state.Buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReceiveCallback), state);
                this._receiveDone.Set();
            }
            catch (Exception ex)
            {
                this.NotifyDisplayMessageOnGUI(string.Format("Error on ReceiveCallback: {0}", ex.ToString()));
            }
        }

        /// <summary>
        /// Handling received message from the Server
        /// </summary>
        /// <param name="handler">The socket to communicate with the Server</param>
        /// <param name="ReceivedMsg">The message which was sent from the Server</param>
        private void MessageFromServerHandler(Socket Handler, string ReceivedMsg)
        {
            // Remove end of string
            var msgComp = ReceivedMsg.Split(new char[] { ':' });
            var msgType = msgComp[0];
            var msgContent = msgComp.Length > 1 ? msgComp[1] : string.Empty;

            switch (msgType)
            {
                case Const.REJECTCONNECT_MSG:
                    this.NotifyConnectStatus(false, ReceivedMsg);
                    break;

                case Const.STARTGAME_MSG:
                    var msgContentComp = msgContent.Split(new char[] { '#' });
                    var subject = msgContentComp[0];
                    var suggestion = msgContentComp[1];
                    var numCharsOfAnswer = int.Parse(msgContentComp[2]);
                    var idPlayer1 = msgContentComp[3];
                    var idPlayer2 = msgContentComp[4];
                    var idplayerAllowTurning = msgContentComp[5];
                    this.NotifyStartGame(subject, suggestion, numCharsOfAnswer, idPlayer1, idPlayer2, idplayerAllowTurning);
                    break;

                case Const.GUESSRESULT_MSG:
                    msgContentComp = msgContent.Split(new char[] { '#' });
                    var idPlayer = msgContentComp[0];
                    var character = msgContentComp[1];
                    var result = bool.Parse(msgContentComp[2]);
                    var positions = msgContentComp[3];
                    this.NotifyAnswerResult(idPlayer, character, result, positions);
                    break;

                case Const.GAMEFINISHED_MSG:
                    this.NotifyGameFinished(msgContent);
                    break;

                case Const.OPPONENTDISCONNECT_MSG:
                    this.NotifyOpponentDisconnected(msgContent);
                    break;

                case Const.UPDATE_ALREADY_PLAYERLIST_MSG:
                    this.NotifyUpdateAlreadyPlayerList(msgContent);
                    break;

                case Const.INVITETOPLAY_MSG:
                    this.NotifyInviteToPlay(msgContent);
                    break;

                case Const.REJECT_INVITETOPLAY_MSG:
                    this.NotifyRejectInviteToPlay(msgContent);
                    break;

                default:
                    this.NotifyDisplayMessageOnGUI(ReceivedMsg);
                    break;
            }
        }

        /// <summary>
        /// Notify to server that the client is going to disconnect 
        /// </summary>  
        /// <param name="PlayerName"></param>
        /// <param name="PlayerIPAddress"></param>
        public void Disconnect(string PlayerName, string PlayerIPAddress)
        {
            var idPlayer = string.Format("{0}@{1}", PlayerName, PlayerIPAddress);
            if (this._socClient != null)
                this.Send(this._socClient, string.Format("DISCONNECT:{0}", idPlayer));
        }

        /// <summary>
        /// Notify to server that the client is going to disconnect 
        /// </summary>
        /// <param name="idPlayer"></param>
        public void Disconnect(string idPlayer)
        {
            if (this._socClient != null)
                this.Send(this._socClient, string.Format("DISCONNECT:{0}", idPlayer));
        }

        /// <summary>
        /// Notify the invitation to play
        /// <param name="msgContent"></param>
        private void NotifyRejectInviteToPlay(string msgContent)
        {
            EventAggregatorFactory.Instance.GetEvent<NotifyRejectInviteToPlayEvent>().Publish(msgContent);
        }


        /// <summary>
        /// Notify the invitation to play
        /// <param name="msgContent"></param>
        private void NotifyInviteToPlay(string msgContent)
        {
            EventAggregatorFactory.Instance.GetEvent<NotifyInviteToPlayEvent>().Publish(msgContent);
        }

        /// <summary>
        /// Notify there is a new player has been connected to the server
        /// <param name="sPlayers"></param>
        private void NotifyUpdateAlreadyPlayerList(string sPlayers)
        {
            EventAggregatorFactory.Instance.GetEvent<NotifyUpdateAlreadyPlayerListEvent>().Publish(sPlayers);
        }

        /// <summary>
        /// Notify timeout occured when establishment communication
        /// </summary>
        /// <param name="idPlayer"></param>
        private void NotifyOpponentDisconnected(string idPlayer)
        {
            EventAggregatorFactory.Instance.GetEvent<NotifyOpponentDisconnectedEvent>().Publish(idPlayer);
        }

        /// <summary>
        /// Notify timeout occured when establishment communication
        /// </summary>
        /// <param name="idWinner"></param>
        private void NotifyGameFinished(string idWinner)
        {
            EventAggregatorFactory.Instance.GetEvent<NotifyGameFinishedEvent>().Publish(idWinner);
        }

        /// <summary>
        /// Notify timeout occured when establishment communication
        /// </summary>
        private void NotifyTimeOut()
        {
            var msg = DateTime.Now.ToString() + " " + "Cannot establish communication to the Server";
            EventAggregatorFactory.Instance.GetEvent<NotifyTimeOutEvent>().Publish(msg);
        }

        /// <summary>
        /// Notify to display message on GUI
        /// </summary>
        /// <param name="msg">The message will be displayed</param>
        private void NotifyDisplayMessageOnGUI(string msg)
        {
            msg = DateTime.Now.ToString() + " " + msg;
            EventAggregatorFactory.Instance.GetEvent<NotifyDisplayMessageOnGUIEvent>().Publish(msg);
        }

        /// <summary>
        /// Notify the connection status
        /// </summary>
        /// <param name="IsConnected">Determines the connection has been established</param>
        /// <param name="MsgTxt">Messagte text</param>
        /// <param name="Name">PLayer Name</param>
        /// <param name="IPAddress">IPAddress of player</param>
        private void NotifyConnectStatus(bool IsConnected, string MsgTxt, string Name = "", string IPAddress = "")
        {
            MsgTxt = DateTime.Now.ToString() + " " + MsgTxt;
            EventAggregatorFactory.Instance.GetEvent<NotifyConnectStatusEvent>().Publish(
                new NotifyConnectStatusEvent.ConnectParam(IsConnected, MsgTxt, new NotifyConnectStatusEvent.PlayerInfo(Name, IPAddress)));
        }

        /// <summary>
        /// Notify start game
        /// </summary>
        /// <param name="Subject"></param>
        /// <param name="Suggestion"></param>
        /// <param name="NumCharsOfAnswer"></param>
        /// <param name="idplayer1"></param>
        /// <param name="idplayer2"></param>
        /// <param name="idplayerAllowTurning"></param>
        private void NotifyStartGame(string Subject, string Suggestion, int NumCharsOfAnswer, string idplayer1, string idplayer2, string idplayerAllowTurning)
        {
            EventAggregatorFactory.Instance.GetEvent<NotifyStartGameEvent>().Publish(
                new NotifyStartGameEvent.GameInfoParam(Subject, Suggestion, NumCharsOfAnswer, idplayer1, idplayer2, idplayerAllowTurning));
        }

        /// <summary>
        /// Notify result of the answer
        /// </summary>
        /// <param name="_idPlayer"></param>
        /// <param name="Character"></param>
        /// <param name="Result"></param>
        /// <param name="Positions"></param>
        private void NotifyAnswerResult(string IDPlayer, string Character, bool Result, string Positions)
        {
            EventAggregatorFactory.Instance.GetEvent<NotifyAnswerResultEvent>().Publish(
                new NotifyAnswerResultEvent.AnswerResultParam(IDPlayer, Character, Result, Positions));
        }

        #endregion
    }

    /// <summary>
    /// State object for reading client data asynchronously
    /// </summary>
    public class StateObject
    {
        /// <summary>
        /// Client  socket.
        /// </summary>
        public Socket WorkSocket = null;

        /// <summary>
        /// Size of receive buffer.
        /// </summary>
        public const int BufferSize = 1024;

        /// <summary>
        /// Receive buffer.
        /// </summary>
        public byte[] Buffer = new byte[BufferSize];

        /// <summary>
        /// Received data string.
        /// </summary>
        public StringBuilder ReceivedMsg = new StringBuilder();
    }
}
